﻿using System;
using System.Collections;
#if WINDOWS_PHONE
using StyleMVVM.Data.Collections;
#else
using System.Collections.Concurrent;
#endif
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using StyleMVVM.Data.Activation;
using StyleMVVM.DependencyInjection;
#if !DOT_NET
using Windows.Foundation.Collections;

#endif

namespace StyleMVVM.Data
{
	/// <summary>
	/// This is the default implementation of the IReflectionService
	/// </summary>
	public sealed class ReflectionService : IReflectionService, IDependencyInjectionContainerAware
	{
		private static readonly ConcurrentBag<Type> _knownTypes = new ConcurrentBag<Type>();
		private static readonly ConstructorInfo exceptionConstructor = null;
		private static readonly PropertyInfo objectArrayIndex = null;

		private readonly ConcurrentDictionary<string, GetPropertyDelegate> getPropertyAccessors;
		private readonly ConcurrentDictionary<string, SetPropertyDelegate> setPropertyAccessors;
		private readonly ConcurrentDictionary<string, CallMethodDelegate> callAccessors;

		static ReflectionService()
		{
			objectArrayIndex = typeof(IList).GetTypeInfo().GetDeclaredProperty("Item");

			foreach (ConstructorInfo declaredConstructor in typeof(Exception).GetTypeInfo().DeclaredConstructors)
			{
				if (declaredConstructor.GetParameters().Count() == 1 &&
				    declaredConstructor.GetParameters().FirstOrDefault().ParameterType == typeof(string))
				{
					exceptionConstructor = declaredConstructor;
				}
			}
		}

		public static void RegisterExport(IDependencyInjectionContainer container)
		{
			container.Register(typeof(ReflectionService)).
			          As(typeof(IReflectionService)).
			          CoreExport(CoreExportStartOrder.First).
			          ActivationMethod("Activate").
			          AndSharedPermenantly();
		}

		public ReflectionService()
		{
			getPropertyAccessors =
				new ConcurrentDictionary<string, GetPropertyDelegate>();

			setPropertyAccessors =
				new ConcurrentDictionary<string, SetPropertyDelegate>();

			callAccessors =
				new ConcurrentDictionary<string, CallMethodDelegate>();
		}

		/// <summary>
		/// List of custom types provided to the <see cref="DataContractSerializer"/> when
		/// reading and writing session state.  Initially empty, additional types may be
		/// added to customize the serialization process.
		/// </summary>
		public static IReadOnlyList<Type> KnownTypes
		{
			get { return new List<Type>(_knownTypes); }
		}

		public static void AddToKnownTypes(Type newType)
		{
			_knownTypes.Add(newType);
		}

		public IActivationService ActivationService { get; set; }

		public IDependencyInjectionContainer Container { get; set; }

		public void Activate()
		{
			ActivationService = Container.LocateByType(typeof(IActivationService)) as IActivationService;
		}

		public object GetPropertyValue(object valueObject, string propertyName)
		{
			return GetPropertyValue(valueObject, propertyName, null, false);
		}

		public object GetPropertyValue(object valueObject, string propertyName, object index, bool throwIfPathMissing)
		{
			if (valueObject == null)
			{
				throw new ArgumentNullException("valueObject");
			}

			if (string.IsNullOrEmpty(propertyName))
			{
				throw new ArgumentNullException("propertyName");
			}

			Type indexType = null;

			if (index != null)
			{
				indexType = index.GetType();
			}

			GetPropertyDelegate getAction = CreateGetPropertyDelegate(valueObject.GetType(), propertyName, indexType);

			if (getAction != null)
			{
				return getAction(valueObject, index, throwIfPathMissing);
			}

			throw new Exception("Could not create property delegate");
		}

		public bool SetPropertyValue(object valueObject, string propertyName, object newValue)
		{
			return SetPropertyValue(valueObject, propertyName, newValue, null, true);
		}

		public bool SetPropertyValue(object valueObject,
		                             string propertyName,
		                             object newValue,
		                             object index,
		                             bool createIfPathMissing)
		{
			if (valueObject == null)
			{
				throw new ArgumentNullException("valueObject");
			}

			if (string.IsNullOrEmpty(propertyName))
			{
				throw new ArgumentNullException("propertyName");
			}

			Type indexType = null;

			if (index != null)
			{
				indexType = index.GetType();
			}

			SetPropertyDelegate setAction = CreateSetPropertyDelegate(valueObject.GetType(), propertyName, indexType);

			if (setAction != null)
			{
				setAction(valueObject, newValue, index, createIfPathMissing);

				return true;
			}

			throw new Exception("Could not create set for property " + propertyName);
		}

		public object CallMethod(object target, string methodName, bool throwIfPathMissing, params object[] parameters)
		{
			if (target == null)
			{
				throw new ArgumentNullException("valueObject");
			}

			if (string.IsNullOrEmpty(methodName))
			{
				throw new ArgumentNullException("methodName");
			}

			CallMethodDelegate callMethod = null;
			string searchMethodName = methodName;

			if (parameters != null && parameters.Length > 0)
			{
				searchMethodName += "|" + parameters.Length;
			}

			if (!callAccessors.TryGetValue(searchMethodName, out callMethod))
			{
				ParameterExpression valueObjectParameter = Expression.Parameter(typeof(object), "valueObject");
				ParameterExpression callParameters = Expression.Parameter(typeof(object[]), "callParameters");
				ParameterExpression throwParameter = Expression.Parameter(typeof(bool), "throwIfMissing");
				ParameterExpression returnValue = Expression.Variable(typeof(object), "returnValue");

				ParameterExpression castValue = Expression.Variable(target.GetType(), "castValue");
				Expression castExpression =
					Expression.Assign(castValue,
					                  Expression.Convert(valueObjectParameter, target.GetType()));

				Expression accessBlock
					= CreateAccessPathCode(castValue,
					                       throwParameter,
					                       target.GetType(),
					                       methodName,
					                       (type, name, expression) =>
						                       {
							                       MethodInfo callMethodInfo = null;

							                       foreach (MethodInfo declaredMethod in
								                       type.GetTypeInfo().GetDeclaredMethods(name))
							                       {
								                       if (parameters != null)
								                       {
									                       if (declaredMethod.GetParameters().Length == parameters.Length)
									                       {
										                       callMethodInfo = declaredMethod;
										                       break;
									                       }
								                       }
								                       else if (declaredMethod.GetParameters().Length == 0)
								                       {
									                       callMethodInfo = declaredMethod;
									                       break;
								                       }
							                       }

							                       if (callMethodInfo == null)
							                       {
								                       throw new Exception(
									                       string.Format("Could not find method {0} on {1}",
									                                     name,
									                                     type.FullName));
							                       }

							                       Expression callExpression = null;

							                       if (parameters != null && parameters.Length > 0)
							                       {
								                       int parmeterIndex = 0;
								                       List<Expression> callParametersList = new List<Expression>();

								                       foreach (var parameterInfo in callMethodInfo.GetParameters())
								                       {
									                       Expression callParameter =
										                       Expression.Convert(
											                       Expression.Property(callParameters,
											                                           objectArrayIndex,
											                                           Expression.Constant(parmeterIndex)),
											                       parameterInfo.ParameterType);

									                       callParametersList.Add(callParameter);

									                       parmeterIndex++;
								                       }

								                       callExpression =
									                       Expression.Call(expression, callMethodInfo, callParametersList);
							                       }
							                       else
							                       {
								                       callExpression =
									                       Expression.Call(expression, callMethodInfo);
							                       }

							                       if (callMethodInfo.ReturnType.Name == "Void")
							                       {
								                       return callExpression;
							                       }
							                       else
							                       {
								                       return Expression.Assign(
									                       returnValue,
									                       Expression.Convert(
										                       callExpression,
										                       typeof(object)));
							                       }
						                       });

				BlockExpression delegatebod =
					Expression.Block(new[] { castValue, returnValue },
					                 castExpression,
					                 accessBlock,
					                 returnValue);

				callMethod = Expression.Lambda<CallMethodDelegate>(
					delegatebod, valueObjectParameter, throwParameter, callParameters).
				                        Compile();

				callAccessors.TryAdd(searchMethodName, callMethod);
			}

			return callMethod(target, throwIfPathMissing, parameters);
		}

		public GetPropertyDelegate CreateGetPropertyDelegate(Type instanceType, string propertyName, Type indexType)
		{
			GetPropertyDelegate getAction = null;
			string methodName = instanceType.FullName + "." + propertyName;

			if (indexType != null)
			{
				methodName += "|" + indexType.FullName;
			}

			if (!getPropertyAccessors.TryGetValue(methodName, out getAction))
			{
				ParameterExpression valueObjectParameter = Expression.Parameter(typeof(object), "valueObject");
				ParameterExpression indexParameter = Expression.Parameter(typeof(object), "index");
				ParameterExpression throwParameter = Expression.Parameter(typeof(bool), "throwIfMissing");
				ParameterExpression returnValueExpression = Expression.Variable(typeof(object), "returnValue");

				ParameterExpression castValue = Expression.Variable(instanceType, "castValue");
				Expression castExpression =
					Expression.Assign(castValue,
					                  Expression.Convert(valueObjectParameter, instanceType));

				Expression accessBlock
					= CreateAccessPathCode(castValue,
					                       throwParameter,
					                       instanceType,
					                       propertyName,
					                       (type, name, expression) =>
						                       {
							                       Expression returnValue = null;
							                       Type fieldOrPropertyType = CreateGetPropertyOrFieldType(type, name);

							                       if (indexType != null)
							                       {
								                       PropertyInfo propertyInfo =
									                       type.GetTypeInfo().GetDeclaredProperty(propertyName);

								                       if (propertyInfo != null)
								                       {
									                       returnValue = Expression.Assign(
										                       returnValueExpression,
										                       Expression.Convert(
											                       Expression.Property(expression,
											                                           propertyInfo,
											                                           Expression.Convert(indexParameter, indexType)),
											                       typeof(object)));
								                       }
								                       else
								                       {
									                       throw new Exception(
										                       string.Format("Could not find property {0} on type {1}",
										                                     type.FullName,
										                                     name));
								                       }
							                       }
							                       else
							                       {
								                       returnValue = Expression.Assign(
									                       returnValueExpression,
									                       Expression.Convert(
										                       Expression.PropertyOrField(expression, name), typeof(object)));
							                       }

							                       return returnValue;
						                       });

				BlockExpression returnBlock =
					Expression.Block(new[] { castValue, returnValueExpression },
					                 castExpression,
					                 accessBlock,
					                 returnValueExpression);

				getAction = Expression.Lambda<GetPropertyDelegate>(
					returnBlock, valueObjectParameter, indexParameter, throwParameter).
				                       Compile();

				getPropertyAccessors.TryAdd(methodName, getAction);
			}

			return getAction;
		}

		public SetPropertyDelegate CreateSetPropertyDelegate(Type instanceType, string propertyName, Type indexType)
		{
			if (instanceType == null)
			{
				throw new ArgumentNullException("instanceType");
			}

			if (string.IsNullOrEmpty(propertyName))
			{
				throw new ArgumentNullException("propertyName");
			}

			SetPropertyDelegate setAction = null;
			string methodName = instanceType.FullName + "." + propertyName;

			if (indexType != null)
			{
				methodName += "|" + indexType.FullName;
			}

			if (!setPropertyAccessors.TryGetValue(methodName, out setAction))
			{
				ParameterExpression valueObjectParameter = Expression.Parameter(typeof(object), "valueObject");
				ParameterExpression newValueParameter = Expression.Parameter(typeof(object), "newValueParameter");
				ParameterExpression indexParameter = Expression.Parameter(typeof(object), "index");
				ParameterExpression createParameter = Expression.Parameter(typeof(bool), "createIfMissing");

				ParameterExpression castValue = Expression.Variable(instanceType, "castValue");
				Expression castExpression =
					Expression.Assign(castValue,
					                  Expression.Convert(valueObjectParameter, instanceType));

				Expression accessBlock
					= CreateAccessPathCode(castValue,
					                       createParameter,
					                       instanceType,
					                       propertyName,
					                       (type, name, expression) =>
						                       {
							                       Expression returnValue = null;

							                       if (indexType != null)
							                       {
								                       PropertyInfo propertyInfo =
									                       type.GetTypeInfo().GetDeclaredProperty(propertyName);

								                       if (propertyInfo != null)
								                       {
									                       returnValue = Expression.Assign(
										                       Expression.Property(expression,
										                                           propertyInfo,
										                                           Expression.Convert(indexParameter, indexType)),
										                       Expression.Convert(newValueParameter, propertyInfo.PropertyType));
								                       }
								                       else
								                       {
									                       throw new Exception(
										                       string.Format("Could not find property {0} on type {1}",
										                                     type.FullName,
										                                     name));
								                       }
							                       }
							                       else
							                       {
								                       Type fieldOrPropertyType = CreateGetPropertyOrFieldType(type, name);

								                       returnValue = Expression.Assign(
									                       Expression.PropertyOrField(expression, name),
									                       Expression.Convert(newValueParameter, fieldOrPropertyType));
							                       }

							                       return returnValue;
						                       });

				BlockExpression delegatebod =
					Expression.Block(new[] { castValue }, castExpression, accessBlock);

				setAction = Expression.Lambda<SetPropertyDelegate>(
					delegatebod, valueObjectParameter, newValueParameter, indexParameter, createParameter).
				                       Compile();

				setPropertyAccessors.TryAdd(methodName, setAction);
			}

			return setAction;
		}

		public static object ExternalGetPropertyInfo(Type baseType, string propertyString)
		{
			return GetPropertyInfo(baseType, propertyString);
		}

		internal static PropertyInfo GetPropertyInfo(Type baseType, string propertyString)
		{
			if (baseType == null)
			{
				throw new ArgumentNullException("baseType");
			}

			if (string.IsNullOrEmpty(propertyString))
			{
				throw new ArgumentNullException("propertyString");
			}

			int periodIndex = propertyString.IndexOf('.');

			if (periodIndex > 0)
			{
				string newPropertyString = propertyString.Substring(0, periodIndex);
				string theRest = propertyString.Substring(periodIndex + 1);

				PropertyInfo propertyInfo =
					baseType.GetTypeInfo().GetDeclaredProperty(newPropertyString);

				if (propertyInfo != null)
				{
					return GetPropertyInfo(propertyInfo.PropertyType, theRest);
				}
			}
			else
			{
				PropertyInfo propertyInfo =
					baseType.GetTypeInfo().GetDeclaredProperty(propertyString);

				if (propertyInfo != null)
				{
					return propertyInfo;
				}
			}

			return null;
		}

		internal static string GetPropertyAccessPath(Expression<Func<object, object>> method)
		{
			if (method == null)
			{
				throw new ArgumentNullException("method");
			}

			MemberExpression memberExpr = null;

			if (method.Body.NodeType == ExpressionType.Convert)
			{
				memberExpr =
					((UnaryExpression)method.Body).Operand as MemberExpression;
			}
			else if (method.Body.NodeType == ExpressionType.MemberAccess)
			{
				memberExpr = method.Body as MemberExpression;
			}

			if (memberExpr == null)
			{
				throw new ArgumentException("method");
			}

			StringBuilder builder = new StringBuilder();

			while (memberExpr != null)
			{
				if (builder.Length != 0)
				{
					builder.Insert(0, '.');
				}

				builder.Insert(0, memberExpr.Member.Name);

				memberExpr = memberExpr.Expression as MemberExpression;
			}

			return builder.ToString();
		}

		public static object ExternalCreateAccessPathCode(object valueParameter,
		                                                  object throwParameter,
		                                                  Type objectType,
		                                                  string propertyName,
		                                                  object createAction)
		{
			return CreateAccessPathCode(valueParameter as ParameterExpression,
			                            throwParameter as ParameterExpression,
			                            objectType,
			                            propertyName,
			                            createAction as Func<Type, string, Expression, Expression>);
		}

		private static Expression CreateAccessPathCode(ParameterExpression valueParameter,
		                                               ParameterExpression throwParameter,
		                                               Type objectType,
		                                               string propertyName,
		                                               Func<Type, string, Expression, Expression> createAction)
		{
			Expression returnExpression = null;
			int firstPeriod = propertyName.IndexOf('.');

			if (firstPeriod > 0)
			{
				string currentPropertyName = propertyName.Substring(0, firstPeriod);
				string theRest = propertyName.Substring(firstPeriod + 1);
				Type propertyOrFieldType = CreateGetPropertyOrFieldType(objectType, currentPropertyName);

				ParameterExpression newValue = Expression.Variable(propertyOrFieldType);

				Expression assignExpression = Expression.Assign(newValue,
				                                                Expression.PropertyOrField(valueParameter, currentPropertyName));

				if (!propertyOrFieldType.GetTypeInfo().IsValueType)
				{
					Expression recurse =
						CreateAccessPathCode(newValue,
						                     throwParameter,
						                     propertyOrFieldType,
						                     theRest,
						                     createAction);

					Expression ifExpression = Expression.IfThenElse(
						Expression.NotEqual(newValue, Expression.Constant(null)),
						recurse,
						Expression.IfThen(Expression.IsTrue(throwParameter),
						                  Expression.Throw(
							                  Expression.New(
								                  exceptionConstructor,
								                  Expression.Constant(
									                  string.Format("Could not find property {1} on type {0}",
									                                objectType.FullName,
									                                currentPropertyName))))));

					returnExpression =
						Expression.Block(new[] { newValue }, new[] { assignExpression, ifExpression });
				}
				else
				{
					Expression recurse = CreateAccessPathCode(newValue,
					                                          throwParameter,
					                                          propertyOrFieldType,
					                                          theRest,
					                                          createAction);

					returnExpression =
						Expression.Block(new[] { newValue }, new[] { assignExpression, recurse });
				}
			}
			else
			{
				returnExpression = createAction(objectType, propertyName, valueParameter);
			}

			return returnExpression;
		}

		private static Type CreateGetPropertyOrFieldType(Type objectType, string propertyName)
		{
			Type propertyOrFieldType = null;

			PropertyInfo propertyInfo =
				objectType.GetTypeInfo().GetDeclaredProperty(propertyName);

			if (propertyInfo == null)
			{
				FieldInfo field = objectType.GetTypeInfo().GetDeclaredField(propertyName);

				if (field != null)
				{
					propertyOrFieldType = field.FieldType;
				}
				else
				{
					throw new Exception(
						string.Format("Could not find property {0} on type {1}",
						              objectType.FullName,
						              propertyName));
				}
			}
			else
			{
				propertyOrFieldType = propertyInfo.PropertyType;
			}

			return propertyOrFieldType;
		}
	}
}